package com.grapecity.forguncy;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.grapecity.forguncy.Entity.PluginItem;
import com.grapecity.forguncy.Entity.Version;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Objects;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class Main {
    public static void main(String[] args) throws Exception {
        try {
            var projectPath = args[0];
            var forguncyVersion = getForguncyVersion(projectPath);
            autoInstallNewPlugin(projectPath, forguncyVersion);
        } catch (Exception e) {
            throw new Exception("插件打包成功，但自动安装出错，请手动安装该插件。");
        }
    }

    private static void autoInstallNewPlugin(String projectPath, String forguncyVersion) throws IOException {

        Path projectPathObj = Paths.get(projectPath);
        var pluginConfigPath = projectPathObj.resolve("src").resolve("main").resolve("resources")
                .resolve("PluginConfig.json");
        var currentPluginConfig = parsePluginConfig(pluginConfigPath);
        var pluginFolder = getPluginFolder(forguncyVersion);

        markPluginRemoved(pluginFolder, currentPluginConfig);
        extractZipToPluginFolder(projectPath, projectPathObj, pluginFolder);
    }

    private static void extractZipToPluginFolder(String projectPath, Path projectPathObj, Path pluginFolder)
            throws IOException {
        var targetPath = projectPathObj.resolve("target");
        var files = new File(targetPath.toString()).listFiles();
        assert files != null;
        var zipFileName = getZipFileName(files);
        if (!zipFileName.isEmpty()) {
            unzip(
                    new FileInputStream(targetPath.resolve(zipFileName).toString()),
                    getTargetFolder(pluginFolder,
                            projectPath.substring(projectPathObj.getParent().toString().length() + 1)));
        }
    }

    private static String getZipFileName(File[] files) {
        var zipFileName = "";
        for (var file : files) {
            String fileName = file.getName().toLowerCase();
            if (fileName.endsWith(".zip")) {
                zipFileName = fileName;
            }
        }
        return zipFileName;
    }

    private static Path getTargetFolder(Path pluginFolder, String dirName) {
        var baseFileName = dirName;
        var targetFolder = pluginFolder.resolve(dirName);
        var index = 1;
        while (Files.exists(targetFolder)) {
            dirName = baseFileName + index;
            targetFolder = pluginFolder.resolve(dirName);
            index++;
        }
        return targetFolder;
    }

    public static void unzip(InputStream is, Path targetDir) throws IOException {
        targetDir = targetDir.toAbsolutePath();
        try (ZipInputStream zipIn = new ZipInputStream(is)) {
            for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) {
                Path resolvedPath = targetDir.resolve(ze.getName()).normalize();
                if (!resolvedPath.startsWith(targetDir)) {
                    // see: https://snyk.io/research/zip-slip-vulnerability
                    throw new RuntimeException("Entry with an illegal path: "
                            + ze.getName());
                }
                if (ze.isDirectory()) {
                    Files.createDirectories(resolvedPath);
                } else {
                    Files.createDirectories(resolvedPath.getParent());
                    Files.copy(zipIn, resolvedPath);
                }
            }
        }
    }

    private static void markPluginRemoved(Path pluginFolder, PluginItem currentPlugin) throws IOException {

        var configs = new ArrayList<String>();
        searchFiles(pluginFolder.toString(), file -> "PluginConfig.json".equals(file.getName()), configs);
        for (var config : configs) {
            Path configPath = Paths.get(config);
            PluginItem pluginItem = parsePluginConfig(configPath);
            if (pluginItem != null) {
                if (shouldRemovePluginFolderWhenReplace(pluginItem, currentPlugin)) {
                    var oldFolder = configPath.getParent();
                    var jarAndDlls = new ArrayList<String>();
                    searchFiles(oldFolder.toString(), Main::fileIsJarOrDll, jarAndDlls);
                    if (checkAllFilesDeletable(jarAndDlls)) {
                        deleteFolder(oldFolder.toString());
                    } else {
                        // 如果现有的插件无法直接删除，就添加删除标记
                        if (!Files.exists(getDeletedMarkerFileName(oldFolder.toString()))) {
                            Files.createFile(getDeletedMarkerFileName(oldFolder.toString()));
                        }
                    }
                }
            }
        }
    }

    public static Path getDeletedMarkerFileName(String config) {
        return Paths.get(config).resolve("Fgc_deleted_mark");
    }

    public static void deleteFolder(String folderPath) {
        File folder = new File(folderPath);

        if (!folder.exists()) {
            return; // 文件夹不存在，直接返回成功
        }

        if (!folder.isDirectory()) {
            return; // 不是一个文件夹，无法删除
        }

        File[] files = folder.listFiles();

        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteFolder(file.getAbsolutePath()); // 递归删除子文件夹
                } else {
                    file.delete(); // 删除文件
                }
            }
        }

        folder.delete();
    }

    private static boolean checkAllFilesDeletable(ArrayList<String> files) {
        var deletable = true;
        for (var filePath : files) {
            var file = new File(filePath);
            deletable = file.renameTo(file);
        }
        return deletable;
    }

    private static boolean fileIsJarOrDll(File file) {
        String fileName = file.getName().toLowerCase();
        return fileName.endsWith(".jar") || fileName.endsWith(".dll");
    }

    public static void searchFiles(String directoryPath, Function<File, Boolean> validFunction,
                                   ArrayList<String> configs) {
        File directory = new File(directoryPath);
        if (directory.isDirectory()) {
            File[] files = directory.listFiles();
            if (files != null) {
                for (File file : files) {
                    if (file.isDirectory()) {
                        searchFiles(file.getAbsolutePath(), validFunction, configs);
                    } else if (validFunction.apply(file)) {
                        configs.add(file.getAbsolutePath());
                    }
                }
            }
        }
    }

    private static Path getPluginFolder(String forguncyVersion) {
        String sharedDocumentsPath = System.getenv("PUBLIC") + "\\Documents";
        var version = new Version(forguncyVersion);
        var pluginFolderName = "ForguncyPluginv" + version.majorVersion
                + (Integer.parseInt(version.patchVersion) >= 100 ? ".1" : "");
        return Paths.get(sharedDocumentsPath).resolve(pluginFolderName);
    }

    // 相同guid的其他插件应该删除，设计器可能加载的是之前的插件。
    public static Boolean shouldRemovePluginFolderWhenReplace(PluginItem newItem, PluginItem deletedItem) {
        return Objects.equals(newItem.guid, deletedItem.guid);
    }

    public static PluginItem parsePluginConfig(Path configPath) {
        var objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
        File file = new File(configPath.toAbsolutePath().toString());
        PluginItem item = null;
        try {
            item = objectMapper.readValue(file, PluginItem.class);
        } catch (IOException e) {
            System.out.println("ParseErrorFilePath:" + configPath);
        }
        if (item != null) {
            item.configFilePath = configPath.toAbsolutePath().toString();
        }
        return item;
    }

    private static String getForguncyVersion(String projectPath)
            throws ParserConfigurationException, IOException, SAXException {
        var version = "";
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        var pomPath = Paths.get(projectPath, "pom.xml").toString();
        var document = builder.parse(new File(pomPath));
        var root = document.getDocumentElement();
        var dependencyList = root.getElementsByTagName("dependency");
        for (int i = 0; i < dependencyList.getLength(); i++) {
            Element dependencyElement = (Element) dependencyList.item(i);
            String groupId = dependencyElement.getElementsByTagName("groupId").item(0).getTextContent();
            if (groupId.equals("com.grapecity.forguncy")) {
                Element systemPathElement = (Element) dependencyElement.getElementsByTagName("version").item(0);
                if (version.isEmpty()) {
                    version = systemPathElement.getTextContent();
                }
            }
        }
        return version;
    }
}